﻿using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web.Security;
using CustomsNuke.Engine.Data;
using CustomsNuke.Engine.Properties;

namespace CustomsNuke.Engine.Security
{
    public class EntityMembershipProvider : MembershipProvider
    {
        #region Fields & Properties

        private readonly CustomsNukeContext _dataContext = new CustomsNukeContext();

        /// <summary>
        /// 	The name of the application using the custom membership provider.
        /// </summary>
        /// <returns>
        /// 	The name of the application using the custom membership provider.
        /// </returns>
        public override string ApplicationName
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        #endregion

        #region User Operations

        /// <summary>
        /// 	Adds a new membership user to the data source.
        /// </summary>
        /// <returns>
        /// 	A <see cref = "T:System.Web.Security.MembershipUser" /> object populated with the information for the newly created user.
        /// </returns>
        /// <param name = "username">The user name for the new user. </param>
        /// <param name = "password">The password for the new user. </param>
        /// <param name = "email">The e-mail address for the new user.</param>
        /// <param name = "passwordQuestion">The password question for the new user.</param>
        /// <param name = "passwordAnswer">The password answer for the new user</param>
        /// <param name = "isApproved">Whether or not the new user is approved to be validated.</param>
        /// <param name = "providerUserKey">The unique identifier from the membership data source for the user.</param>
        /// <param name = "status">A <see cref = "T:System.Web.Security.MembershipCreateStatus" /> enumeration value indicating whether the user was created successfully.</param>
        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            if (!SecUtility.ValidateParameter(ref password, true, true, false, 128))
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }
            if (passwordAnswer != null)
            {
                passwordAnswer = passwordAnswer.Trim();
            }
            if (!string.IsNullOrEmpty(passwordAnswer))
            {
                if (passwordAnswer.Length > 128)
                {
                    status = MembershipCreateStatus.InvalidAnswer;
                    return null;
                }
            }
            if (!SecUtility.ValidateParameter(ref passwordAnswer, RequiresQuestionAndAnswer, true, false, 128))
            {
                status = MembershipCreateStatus.InvalidAnswer;
                return null;
            }
            if (!SecUtility.ValidateParameter(ref email, RequiresUniqueEmail, RequiresUniqueEmail, false, 256))
            {
                status = MembershipCreateStatus.InvalidEmail;
                return null;
            }
            if (!SecUtility.ValidateParameter(ref passwordQuestion, RequiresQuestionAndAnswer, true, false, 256))
            {
                status = MembershipCreateStatus.InvalidQuestion;
                return null;
            }
            if ((providerUserKey != null) && !(providerUserKey is Guid))
            {
                status = MembershipCreateStatus.InvalidProviderUserKey;
                return null;
            }
            if (password.Length < MinRequiredPasswordLength)
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }
            var nonAlphanumericCharacters = password.Where((t, index) => !char.IsLetterOrDigit(password, index)).Count();
            if (nonAlphanumericCharacters < MinRequiredNonAlphanumericCharacters)
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }
            if ((PasswordStrengthRegularExpression.Length > 0) && !Regex.IsMatch(password, PasswordStrengthRegularExpression))
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }
            var e = new ValidatePasswordEventArgs(username, password, true);
            OnValidatingPassword(e);
            if (e.Cancel)
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }
            status = MembershipCreateStatus.Success;
            return null;
        }

        /// <summary>
        /// 	Updates information about a user in the data source.
        /// </summary>
        /// <param name = "user">A <see cref = "T:System.Web.Security.MembershipUser" /> object that represents the user to update and the updated information for the user. </param>
        public override void UpdateUser(MembershipUser user)
        {
            if (user == null)
            {
                throw new ArgumentNullException("user");
            }
            var username = user.UserName;
            SecUtility.CheckParameter(ref username, true, true, true, 256, "UserName");
            var email = user.Email;
            SecUtility.CheckParameter(ref email, RequiresUniqueEmail, RequiresUniqueEmail, false, 256, "Email");
        }

        /// <summary>
        /// 	Verifies that the specified user name and password exist in the data source.
        /// </summary>
        /// <returns>
        /// 	true if the specified username and password are valid; otherwise, false.
        /// </returns>
        /// <param name = "username">The name of the user to validate. </param>
        /// <param name = "password">The password for the specified user. </param>
        public override bool ValidateUser(string username, string password)
        {
            return SecUtility.ValidateParameter(ref username, true, true, true, 256) && SecUtility.ValidateParameter(ref password, true, true, false, 128) && CheckPassword(username, password, true, true);
        }

        /// <summary>
        /// 	Clears a lock so that the membership user can be validated.
        /// </summary>
        /// <returns>
        /// 	true if the membership user was successfully unlocked; otherwise, false.
        /// </returns>
        /// <param name = "username">The membership user whose lock status you want to clear.</param>
        public override bool UnlockUser(string username)
        {
            SecUtility.CheckParameter(ref username, true, true, true, 256, "username");
            try
            {
                var currentUser = _dataContext.Users.FirstOrDefault(user => user.UserName == username && user.IsDeleted == false);
                if (currentUser != null)
                {
                    currentUser.IsLockedOut = false;
                    currentUser.FailedPasswordAttemptCount = 0;
                    currentUser.FailedPasswordAttemptWindowStart = null;
                    currentUser.FailedPasswordAnswerAttemptCount = 0;
                    currentUser.FailedPasswordAnswerAttemptWindowStart = null;
                    currentUser.LastLockoutDate = DateTime.Now;

                    currentUser.ModifiedDate = DateTime.Now;
                    _dataContext.SaveChanges();
                    return true;
                }
                return false;
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException(string.Format(Resources.MembershipCannotUnlockUser, username), ex);
            }
        }

        /// <summary>
        /// 	Gets user information from the data source based on the unique identifier for the membership user. Provides an option to update the last-activity date/time stamp for the user.
        /// </summary>
        /// <returns>
        /// 	A <see cref = "T:System.Web.Security.MembershipUser" /> object populated with the specified user's information from the data source.
        /// </returns>
        /// <param name = "providerUserKey">The unique identifier for the membership user to get information for.</param>
        /// <param name = "userIsOnline">true to update the last-activity date/time stamp for the user; false to return user information without updating the last-activity date/time stamp for the user.</param>
        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// 	Gets information from the data source for a user. Provides an option to update the last-activity date/time stamp for the user.
        /// </summary>
        /// <returns>
        /// 	A <see cref = "T:System.Web.Security.MembershipUser" /> object populated with the specified user's information from the data source.
        /// </returns>
        /// <param name = "username">The name of the user to get information for. </param>
        /// <param name = "userIsOnline">true to update the last-activity date/time stamp for the user; false to return user information without updating the last-activity date/time stamp for the user. </param>
        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// 	Gets the user name associated with the specified e-mail address.
        /// </summary>
        /// <returns>
        /// 	The user name associated with the specified e-mail address. If no match is found, return null.
        /// </returns>
        /// <param name = "email">The e-mail address to search for. </param>
        public override string GetUserNameByEmail(string email)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// 	Removes a user from the membership data source.
        /// </summary>
        /// <returns>
        /// 	true if the user was successfully deleted; otherwise, false.
        /// </returns>
        /// <param name = "username">The name of the user to delete.</param>
        /// <param name = "deleteAllRelatedData">true to delete data related to the user from the database; false to leave data related to the user in the database.</param>
        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// 	Gets a collection of all the users in the data source in pages of data.
        /// </summary>
        /// <returns>
        /// 	A <see cref = "T:System.Web.Security.MembershipUserCollection" /> collection that contains a page of <paramref
        ///  	name = "pageSize" /><see cref = "T:System.Web.Security.MembershipUser" /> objects beginning at the page specified by <paramref
        ///  	name = "pageIndex" />.
        /// </returns>
        /// <param name = "pageIndex">The index of the page of results to return. <paramref name = "pageIndex" /> is zero-based.</param>
        /// <param name = "pageSize">The size of the page of results to return.</param>
        /// <param name = "totalRecords">The total number of matched users.</param>
        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// 	Gets the number of users currently accessing the application.
        /// </summary>
        /// <returns>
        /// 	The number of users currently accessing the application.
        /// </returns>
        public override int GetNumberOfUsersOnline()
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// 	Gets a collection of membership users where the user name contains the specified user name to match.
        /// </summary>
        /// <returns>
        /// 	A <see cref = "T:System.Web.Security.MembershipUserCollection" /> collection that contains a page of <paramref
        ///  	name = "pageSize" /><see cref = "T:System.Web.Security.MembershipUser" /> objects beginning at the page specified by <paramref
        ///  	name = "pageIndex" />.
        /// </returns>
        /// <param name = "usernameToMatch">The user name to search for.</param>
        /// <param name = "pageIndex">The index of the page of results to return. <paramref name = "pageIndex" /> is zero-based.</param>
        /// <param name = "pageSize">The size of the page of results to return.</param>
        /// <param name = "totalRecords">The total number of matched users.</param>
        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// 	Gets a collection of membership users where the e-mail address contains the specified e-mail address to match.
        /// </summary>
        /// <returns>
        /// 	A <see cref = "T:System.Web.Security.MembershipUserCollection" /> collection that contains a page of <paramref
        ///  	name = "pageSize" /><see cref = "T:System.Web.Security.MembershipUser" /> objects beginning at the page specified by <paramref
        ///  	name = "pageIndex" />.
        /// </returns>
        /// <param name = "emailToMatch">The e-mail address to search for.</param>
        /// <param name = "pageIndex">The index of the page of results to return. <paramref name = "pageIndex" /> is zero-based.</param>
        /// <param name = "pageSize">The size of the page of results to return.</param>
        /// <param name = "totalRecords">The total number of matched users.</param>
        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        #endregion

        #region Password Properties

        /// <summary>
        /// 	Indicates whether the membership provider is configured to allow users to retrieve their passwords.
        /// </summary>
        /// <returns>
        /// 	true if the membership provider is configured to support password retrieval; otherwise, false. The default is false.
        /// </returns>
        public override bool EnablePasswordRetrieval
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// 	Indicates whether the membership provider is configured to allow users to reset their passwords.
        /// </summary>
        /// <returns>
        /// 	true if the membership provider supports password reset; otherwise, false. The default is true.
        /// </returns>
        public override bool EnablePasswordReset
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// 	Gets a value indicating whether the membership provider is configured to require the user to answer a password question for password reset and retrieval.
        /// </summary>
        /// <returns>
        /// 	true if a password answer is required for password reset and retrieval; otherwise, false. The default is true.
        /// </returns>
        public override bool RequiresQuestionAndAnswer
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// 	Gets the number of invalid password or password-answer attempts allowed before the membership user is locked out.
        /// </summary>
        /// <returns>
        /// 	The number of invalid password or password-answer attempts allowed before the membership user is locked out.
        /// </returns>
        public override int MaxInvalidPasswordAttempts
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// 	Gets the number of minutes in which a maximum number of invalid password or password-answer attempts are allowed before the membership user is locked out.
        /// </summary>
        /// <returns>
        /// 	The number of minutes in which a maximum number of invalid password or password-answer attempts are allowed before the membership user is locked out.
        /// </returns>
        public override int PasswordAttemptWindow
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// 	Gets a value indicating whether the membership provider is configured to require a unique e-mail address for each user name.
        /// </summary>
        /// <returns>
        /// 	true if the membership provider requires a unique e-mail address; otherwise, false. The default is true.
        /// </returns>
        public override bool RequiresUniqueEmail
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// 	Gets a value indicating the format for storing passwords in the membership data store.
        /// </summary>
        /// <returns>
        /// 	One of the <see cref = "T:System.Web.Security.MembershipPasswordFormat" /> values indicating the format for storing passwords in the data store.
        /// </returns>
        public override MembershipPasswordFormat PasswordFormat
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// 	Gets the minimum length required for a password.
        /// </summary>
        /// <returns>
        /// 	The minimum length required for a password. 
        /// </returns>
        public override int MinRequiredPasswordLength
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// 	Gets the minimum number of special characters that must be present in a valid password.
        /// </summary>
        /// <returns>
        /// 	The minimum number of special characters that must be present in a valid password.
        /// </returns>
        public override int MinRequiredNonAlphanumericCharacters
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// 	Gets the regular expression used to evaluate a password.
        /// </summary>
        /// <returns>
        /// 	A regular expression used to evaluate a password.
        /// </returns>
        public override string PasswordStrengthRegularExpression
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        #endregion

        #region Password Operations

        /// <summary>
        /// 	Processes a request to update the password question and answer for a membership user.
        /// </summary>
        /// <returns>
        /// 	true if the password question and answer are updated successfully; otherwise, false.
        /// </returns>
        /// <param name = "username">The user to change the password question and answer for. </param>
        /// <param name = "password">The password for the specified user. </param>
        /// <param name = "newPasswordQuestion">The new password question for the specified user. </param>
        /// <param name = "newPasswordAnswer">The new password answer for the specified user. </param>
        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// 	Gets the password for the specified user name from the data source.
        /// </summary>
        /// <returns>
        /// 	The password for the specified user name.
        /// </returns>
        /// <param name = "username">The user to retrieve the password for. </param>
        /// <param name = "answer">The password answer for the user. </param>
        public override string GetPassword(string username, string answer)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// 	Processes a request to update the password for a membership user.
        /// </summary>
        /// <returns>
        /// 	true if the password was updated successfully; otherwise, false.
        /// </returns>
        /// <param name = "username">The user to update the password for. </param>
        /// <param name = "oldPassword">The current password for the specified user. </param>
        /// <param name = "newPassword">The new password for the specified user. </param>
        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// 	Resets a user's password to a new, automatically generated password.
        /// </summary>
        /// <returns>
        /// 	The new password for the specified user.
        /// </returns>
        /// <param name = "username">The user to reset the password for. </param>
        /// <param name = "answer">The password answer for the specified user. </param>
        public override string ResetPassword(string username, string answer)
        {
            throw new NotImplementedException();
        }

        private bool CheckPassword(string username, string password, bool updateLastLoginActivityDate, bool failIfNotApproved)
        {
            string passwordSalt;
            int passwordFormat;
            return CheckPassword(username, password, updateLastLoginActivityDate, failIfNotApproved, out passwordSalt, out passwordFormat);
        }

        private bool CheckPassword(string username, string password, bool updateLastLoginActivityDate, bool failIfNotApproved, out string passwordSalt, out int passwordFormat)
        {
            string checkingPassword;
            int status;
            int failedPasswordAttemptCount;
            int failedPasswordAnswerAttemptCount;
            bool isApproved;
            DateTime? lastLoginDate;
            DateTime? lastActivityDate;
            GetPasswordWithFormat(username, updateLastLoginActivityDate, out status, out checkingPassword, out passwordFormat, out passwordSalt, out failedPasswordAttemptCount, out failedPasswordAnswerAttemptCount, out isApproved, out lastLoginDate, out lastActivityDate);
            if (status != 0)
            {
                return false;
            }
            if (!isApproved && failIfNotApproved)
            {
                return false;
            }
            var passwordCorrect = password.Equals(checkingPassword);
            if (passwordCorrect && failedPasswordAttemptCount == 0 && failedPasswordAnswerAttemptCount == 0)
            {
                return true;
            }
            UpdateUserInfo(username, passwordCorrect, updateLastLoginActivityDate, MaxInvalidPasswordAttempts, PasswordAttemptWindow, DateTime.Now, passwordCorrect ? DateTime.Now : lastLoginDate, passwordCorrect ? DateTime.Now : lastActivityDate);
            return passwordCorrect;
        }

        private void UpdateUserInfo(string username, bool passwordCorrect, bool updateLastLoginActivityDate, int maxInvalidPasswordAttempts, int passwordAttemptWindow, DateTime currentTime, DateTime? lastLoginDate, DateTime? lastActivityDate)
        {
            try
            {
                var currentUser = _dataContext.Users.FirstOrDefault(user => user.UserName == username && user.IsDeleted == false);
                if (currentUser != null && !currentUser.IsLockedOut)
                {
                    if (!passwordCorrect)
                    {
                        if (currentUser.FailedPasswordAttemptWindowStart.HasValue && currentTime > currentUser.FailedPasswordAttemptWindowStart.Value.AddMinutes(passwordAttemptWindow))
                        {
                            currentUser.FailedPasswordAttemptWindowStart = currentTime;
                            currentUser.FailedPasswordAttemptCount = 1;
                        }
                        else
                        {
                            currentUser.FailedPasswordAttemptWindowStart = currentTime;
                            currentUser.FailedPasswordAttemptCount = currentUser.FailedPasswordAttemptCount + 1;
                        }
                        if (currentUser.FailedPasswordAttemptCount >= maxInvalidPasswordAttempts)
                        {
                            currentUser.IsLockedOut = true;
                            currentUser.LastLockoutDate = currentTime;
                        }
                    }
                    else
                    {
                        currentUser.FailedPasswordAttemptWindowStart = null;
                        currentUser.FailedPasswordAttemptCount = 0;
                        currentUser.FailedPasswordAnswerAttemptWindowStart = null;
                        currentUser.FailedPasswordAnswerAttemptCount = 0;
                    }
                    if (updateLastLoginActivityDate)
                    {
                        currentUser.LastActivityDate = lastActivityDate;
                        currentUser.LastLoginDate = lastLoginDate;
                    }
                    currentUser.ModifiedDate = DateTime.Now;
                    _dataContext.SaveChanges();
                }
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException(string.Format(Resources.MembershipCannotUpdateUserInfo, username), ex);
            }
        }

        private void GetPasswordWithFormat(string username, bool updateLastLoginActivityDate, out int status, out string password, out int passwordFormat, out string passwordSalt, out int failedPasswordAttemptCount, out int failedPasswordAnswerAttemptCount, out bool isApproved, out DateTime? lastLoginDate, out DateTime? lastActivityDate)
        {
            try
            {
                var currentUser = _dataContext.Users.FirstOrDefault(user => user.UserName == username && user.IsDeleted == false);
                status = 0;
                if (currentUser != null)
                {
                    if (currentUser.IsLockedOut)
                    {
                        status = 99;
                    }
                    password = currentUser.Password;
                    passwordFormat = 0;
                    passwordSalt = null;
                    failedPasswordAttemptCount = currentUser.FailedPasswordAttemptCount;
                    failedPasswordAnswerAttemptCount = currentUser.FailedPasswordAnswerAttemptCount;
                    isApproved = currentUser.IsApproved;
                    lastLoginDate = currentUser.LastLoginDate;
                    lastActivityDate = currentUser.LastActivityDate;

                    if (updateLastLoginActivityDate && isApproved)
                    {
                        currentUser.LastLoginDate = DateTime.Now;
                        currentUser.LastActivityDate = DateTime.Now;
                        currentUser.ModifiedDate = DateTime.Now;
                        _dataContext.SaveChanges();
                    }
                }
                else
                {
                    status = 1;
                    password = null;
                    passwordFormat = 0;
                    passwordSalt = null;
                    failedPasswordAttemptCount = 0;
                    failedPasswordAnswerAttemptCount = 0;
                    isApproved = false;
                    lastLoginDate = null;
                    lastActivityDate = null;
                }
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException(string.Format(Resources.MembershipCannotGetPasswordWithFormat, username), ex);
            }
        }

        #endregion
    }
}